前言

代码演示环境

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

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

界面和交互

AWT事件模型

如果一个人玩橡棋就像一个人玩游戏时没有交互一样,会非常无聊,所以玩家最大的乐趣就是与电脑或者人的交互。那么首先玩家得与电脑交互—键盘与鼠标的交互,在JDK 1.4版本还提供了手柄的驱动让玩家与电脑交互。

AWT有自己的事件分发线程—该线程分发所有种类的事件,比如鼠标点击和键盘事件,这些事件都来自于操作系统。

那么AWT在哪里分发这些事件?在一个特定的组件出现一种事件时分发。AWT会检查是否有该事件的监听器存在—监听器是一个对象,它专门从另外一个对象接收事件,在这种情况下,事件就会来自于AWT事件分发器线程了。每种事件都有对应的监听器,比如输入事件,我们有KeyListener接口来对象。下面描述的是事件的工作流程:

  • 用户按下键
  • 操作系统发送键盘事件给Java运行时
  • java运行时产生事件对象,然后添加到AWT的事件队列中去
  • AWT事件分发送线程分配事件对象给任何一个KeyListeners
  • KeyListener获取键盘事件,并且做它想做的事

我们可以使用AWTEventListener类,它可以用来调试处理

Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener(){
	public void eventDispatched(AWTEevent event){
		System.out.println(event);
	}
}, -1);

注意:以上代码只能用来调试,但不能使用在真实的游戏中。

键盘输入

在一个游戏中,我们会使用大量的键盘,比如光标键来移动人物的位置,以及使用键盘控制武器。下面我们使用KeyListener来监听键盘事件,并且处理这些事件。

Window window = screen.getFullScreenWindow();
window.addKeyListener(keyListener);

KeyListener接口定义了keyPressed(), KeyReleased()和KeyTyped()方法。“typed”事件出现一个键盘第一次按下之后,然后重复点击该键盘。该事件对于游戏来基本上没有使用,所以我们只关注键盘的press和release事件。

以上方法都有一个KeyEvent事件参数,该事件对象可以让我们观察哪个键盘被按下和释放掉—使用虚拟键盘代码(virtual key code)。虚拟键盘是Java定义的代码,用来表示每个键盘的键,但是它不与实际的字符相同,比如Q和q是不同字符,但是它们有相同的key code值。所有的虚拟键盘都是以VK_xxx表示,比如Q键使用KeyEvent.VK_Q表示,大多数情况下,我们可以根据虚拟键来推判实际对应的键。注意:Window类的setFocusTraversalKeysEnabled(false)方法是让按键聚焦在转换键事件上,转换键可以修改当前按键的焦点,然后可以让焦点移到另外的组件中去。比如,在一个web网页中,我们可能按了Tab键,让光标从一个表单域移到另外一个表单域组件中去。Tab键的事件由AWT的焦点转换代码封装,但是我们可获取Tab键的事件,所以这个方法允许我们可以这样使用。除了Tab键,我们可以使用Alt键来产生激活记忆行为(activate nmemonic)。比如按Alt+F是激活File菜单行为。因为AWT会认为在Alt之后按下的键会被忽略,所以如果不想有这种结果我们会呼叫KeyEvent的consume()方法不让AWT忽略该行为。在确认没有其它对象处理Alt键(或者没有修饰键激活记忆),那么我们把Alt键看成一个普通的键。

键盘演示代码-KeyTest

package com.funfree.arklis.input;
import java.awt.event.*;
import java.awt.*;
import java.util.LinkedList;
import com.funfree.arklis.util.*;
import com.funfree.arklis.engine.*;
/**
	功能:书写一个键盘测试类,用来说明键盘事件的使用
	备注:该类继承GameCore引擎类,然后实现键盘监听器接口,以处理键盘操作事件。
	*/

public class KeyTest extends GameCore {
	private LinkedList messages = new LinkedList();//使用一个双向链表来保存事件
	
	/**
		重写你父类的init方法,以初始化本类的实例。
		*/
	public void init(){
		super.init();
		//设置屏幕为全屏幕显示
		Window window = screen.getFullScreenWindow();
		//允许输入TAB键和其它特定键
		window.setFocusTraversalKeysEnabled(false);
		//给当前全屏幕添加键盘监听器
		window.addKeyListener(this);
		//向集合中添加消息
		addMessage("键盘输入测试,按Escape键退出程序。");
	}
	
	/*
		实现监听器接口定义的方法
		*/
	public void keyPressed(KeyEvent event){
		int keyCode = event.getKeyCode();
		//如果按了esc键
		if(keyCode == KeyEvent.VK_ESCAPE){
			stop();//那么设置结果标识位
		}else{
			//否则处理按下事件
			addMessage("按下了:" + KeyEvent.getKeyText(keyCode));
			//event.consume();//确定该键不处理任何事件
		}
	}
	
	public void keyReleased(KeyEvent event){
		int keyCode = event.getKeyCode();
		addMessage("释放了:" + KeyEvent.getKeyText(keyCode));
		//event.consume();
	}
	
	public void keyTyped(KeyEvent event){
		//event.consume();
	}
	
	public synchronized void addMessage(String message){
		messages.add(message);
		//如果集合的大小大于或者等于屏幕的高度除了字体大小
		if(messages.size() >= screen.getHeight() / FONT_SIZE){
			messages.remove(0); //那么删除集合中的第
		}
	}
	
	/**
		绘制集合听元素,其中RenderingHints类定义和管理键和关联值的集合,它允许
		应用程序将输入参数作为其它类使用的算法选择,这些类用来执行呈现和图片处理服务。
		*/
	public synchronized void draw(Graphics2D g){
		Window window = screen.getFullScreenWindow();
		//使用指定的算法实现图像的显示--要求“文本抗锯齿提示键”和"文本抗锯齿提示值"
		g.setRenderingHint(
			RenderingHints.KEY_TEXT_ANTIALIASING,
			RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		//绘制背景图像
		g.setColor(window.getBackground());
		g.fillRect(0,0,screen.getWidth(),screen.getHeight());
		//绘制需要显示的消息
		g.setColor(window.getForeground());
		int y = FONT_SIZE;
		//绘制文字在互屏幕中去
		for(int i = 0; i < messages.size(); i++){
			g.drawString((String)messages.get(i),5,y);
			y += FONT_SIZE;
		}
	}
}

关联核心代码-GameCore

package com.funfree.arklis.engine;
import static java.lang.System.*;
import java.awt.*;
import com.funfree.arklis.util.*;
import javax.swing.ImageIcon;
import java.util.*;
import com.funfree.arklis.input.*;

/**
	功能:书写一个抽象类,用来测试它的子类实现draw方法
	备注:
		  该类是一个引擎,它规定了子类实现游戏的动作:重写update方法和draw方法。客户端类只需要实现
		  gameLoop中的update方法与draw方法即可。如果需要实现与用户的交互,那么只需要向子类添加相应
		  的监听器即可。
	*/
public abstract class GameCore extends ActionAdapter{
	protected static final int FONT_SIZE = 54;
	private boolean isRunning;
	protected ScreenManager screen; //有屏幕管理
	protected InputManager inputManager;//有输入管理器
	//用来保存引擎的组件,比如InputComponent等
	protected java.util.List list; //使用时用来再初始化
	
	public void setList(java.util.List list){
		this.list = list;
	}
	
	public java.util.List getList(){
		return list;
	}
	
	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 ScreenManager getScreenManager(){
		return screen;
	}
	
	
	/**
		表示游戏结束
		*/
	public void stop(){
		isRunning = false;
	}
	
	/**
		呼叫init()和gameLoop()方法
		*/
	public void run(){
		try{
			init();
			gameLoop();
		}finally{
			screen.restoreScreen();
		}
	}
	//默认的初始化行为
	public void init(){
		//1. 指定一个屏幕管理器对象
		screen = new ScreenManager();
		//2. 然后确定当前计算机的显卡
		DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES);
		//3. 设置全屏幕显示模型--它是子类获取全屏幕的前提
		screen.setFullScreen(displayMode);
		//4.下面是获取全屏幕中的默认字体样式与颜色
		Window window = screen.getFullScreenWindow();
		window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
		window.setBackground(Color.blue);
		window.setForeground(Color.white);
		//5. 表示当前游戏运行中
		isRunning = true;
	}
	
	public Image loadImage(String fileName){
		return new ImageIcon(fileName).getImage();
	}
	
	/**
		如果stop方法被呼叫,那么停止呼叫该方法。默认的gameLoop()行为。
		*/
	private void gameLoop(){
		//获取当前的时间
		long startTime = currentTimeMillis();
		//初始化游戏开始的当前
		long currentTime = startTime;
		//如果isRunning为true值
		while(isRunning){//那么让游戏循环继续
			//1. 当前游戏进行时间--其中elapsedTime值的大小是由当前
			//	主线程sleep的值(Thread.sleep(20))来确定的!
			long elapsedTime = currentTimeMillis() - currentTime;
			out.println("当前时间:" + currentTime + ",游戏的进行的时间:" + elapsedTime);
			currentTime += elapsedTime;
			//2. 根据当前游戏的进行时间来进行游戏动画的更新--需要子类重写(指定的动作)
			update(elapsedTime);
			Graphics2D g = screen.getGraphics();
			draw(g);//绘制图片--需要子类重写(指定的动作)
			g.dispose();
			screen.update();//使用双缓存技术刷新屏幕
			try{
				Thread.sleep(20);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}//否则不作为!
	}
	
	/**
		功能:该方法需要由子类实现,以实现特定的动画效果。具体的动画效果,需要根据需求描述来实现。
			可以写成抽象方法作为框架来使用!
		*/
	public void update(long elapsedTime){
		//do nothing
	}
	
	/**
		功能:定义一个抽象方法,要求子类必须实现该方法,以便能够在屏幕中显示出来。该方法必须实现
		*/
	public abstract void draw(Graphics2D g);
}

键盘输入运行效果

Java游戏编程不完全详解-3_游戏开发

鼠标输入

鼠标有三种事件:

  • 鼠标按钮点击事件
  • 鼠标移动事件
  • 鼠标滚动事件

鼠标演示代码-MouseTest

package com.funfree.arklis.input;
import java.awt.event.*;
import java.awt.*;
import java.util.LinkedList;
import com.funfree.arklis.util.*;
import com.funfree.arklis.engine.*;

/**
	功能:书写一个类用来测试监听鼠标的行为
	备注:继承游戏引擎GameCore父类,然后实现键盘监听器,鼠标相关的监听器(包括鼠标移动、
		  鼠标滚轴监听器)
	*/
public class MouseTest extends GameCore {
	private static final int TRAIL_SIZE = 10; //绘制重影10个
	private static final Color[] COLORS = { //设置字体的前景颜色
		Color.white, Color.black, Color.yellow, Color.magenta
	};
	private LinkedList trailList;
	private boolean trailMode;
	private int colorIndex;
	
	/**
		重写init方法以初始化该类的实例
		*/
	public void init(){
		super.init();
		trailList = new LinkedList();
		Window window = screen.getFullScreenWindow();
		//给当前全屏幕添加鼠标和键盘的监听器
		window.addMouseListener(this);
		window.addMouseMotionListener(this);
		window.addMouseWheelListener(this);
		window.addKeyListener(this);
	}
	
	/**
		功能:重写/实现draw的抽象方法,以实现鼠标的draw动作。
		*/
	public synchronized void draw(Graphics2D g){
		int count = trailList.size();
		//是否连续绘制当前移动的鼠标
		if(count > 1 && !trailMode){
			count = 1;//只绘制第一个Point对象字样
		}
		//1. 获取当前的全屏幕
		Window window = screen.getFullScreenWindow();
		
		//2. 然后向该全屏幕绘制背景--必须先有这一步
		g.setColor(window.getBackground());
		g.fillRect(0,0,screen.getWidth(),screen.getHeight());
		//3. 接着绘制指令--指绘制文本需要抗锯齿效果
		g.setRenderingHint(
			RenderingHints.KEY_TEXT_ANTIALIASING,
			RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		g.setColor(window.getForeground());
		//4. 开始绘制文本到全屏幕中
		g.drawString("鼠标测试。按Escape键退出程序。", 5, FONT_SIZE);
		//绘制鼠标--根据鼠标当前的位置来绘制句文字--绘制"你好!Java世界。"重影效果
		for(int i = 0; i < count; i++){
			Point point = (Point)trailList.get(i);
			g.drawString("你好!Java世界。",point.x, point.y);
		}
	}
	
	//判断是否为重影显示“你好!Java世界。”字样
	public void mousePressed(MouseEvent event){
		trailMode = !trailMode;
	}
	
	//重写鼠标进入事件
	public void mouseEntered(MouseEvent event){
		mouseMoved(event);
	}
	
	//重写鼠标拖拽事件
	public void mouseDragged(MouseEvent event){
		mouseMoved(event);
	}
	
	//重写鼠标退出事件
	public void mouseExited(MouseEvent event){
		mouseMoved(event);
	}
	
	/**
		重写鼠标移动事件,用来保存当前鼠标的移动的坐标值,这些坐标的个数必须小于TRAIL_SIZE的值
		*/
	public synchronized void mouseMoved(MouseEvent event){
		Point point = new Point(event.getX(), event.getY());
		trailList.addFirst(point);
		while(trailList.size() > TRAIL_SIZE){
			trailList.removeLast();
		}
	}
	
	/**
		重写鼠标的滚轴事件,用来处理屏幕中前景显示的颜色。
		*/
	public void mouseWheelMoved(MouseWheelEvent event){
		colorIndex = (colorIndex + event.getWheelRotation()) % COLORS.length;
		if(colorIndex < 0){
			colorIndex  += COLORS.length;
		}
		Window window = screen.getFullScreenWindow();
		window.setForeground(COLORS[colorIndex]);
	}
	
	//重写键盘的按下事件,以便退出应用程序
	public void keyPressed(KeyEvent event){
		//如果按下了Esc键,那么屏幕进入游戏前的显示模型,并结束程序。
		if(event.getKeyCode() == KeyEvent.VK_ESCAPE){
			stop();
		}
	}
	
}

Point对象被保存到trailList集合中,该对象有x和y的坐标值,并且最多可以保存10个坐标值。如果鼠标移动在继续,那么draw方法会给每个Point绘制一个“hello world!”字样,否则只绘制第一个Point对象,点击鼠标会修改trail模型。

Java游戏编程不完全详解-3_游戏开发_02

在以上代码中,我们Robot类移动鼠标,但是鼠标移动事件可能不会立即出现,所以代码会检查鼠标移动事件是否定位在屏幕中央。如果是这样,那么把它认为是一种重置中央的事件,而实际的事件被忽略掉;否则该事件被当作普通的鼠标移动事件处理。 对于鼠标的样子,我们可以使用Java API创建自己的样式,创建时需要使用Toolkit类的createCustomerCursor()方法来实现

Java游戏编程不完全详解-3_游戏开发_03

在游戏中我们可以呼叫Toolkit类截取一个不可见的光标,然后呼叫setCursor()方法:

Window window = screen.getFullScreenWindow();
window.setCursor(invisibleCursor);

之后,我们可以呼叫Cursor类的getPredefinedCursor()方法来恢复原来的光标样式:

Cursor normalCursor = Cursor.getPredefineCursor(Cursor.DEFAULT_CURSOR);

创建输入管理器

前面讲解了常用的输入事件,以及它们的处理。下面我们把它们放在一起,就可以创建一个输入管理器。但是,在封装之前,我们先要说明前面的代码的缺陷。

首先,我们应该注意到synchronized修饰的方法。记住:所有的事件都是从AWT事件分发线程中产生的,该线程不是主线程!显然,我们不修改游戏状态(修改妖怪的位置),所以这些同步方法肯定不可能让这些事件发生。而在我们的实际游戏中,我们必须处理游戏循环中的特定的点(point)。

所以,为了解决这个问题,我们需要设置标识位(boolean变量)来标识,这个标识变量的修改发生键盘按下事件。比如jumpIsPressed布尔值可以在keyPressed()方法中设置和修改,然后在后面的游戏循环(game loop)中检查该变量是否被设置了,然后再根据这个标识呼叫相应的代码来处理游戏的行为。对于有些行为,比如“跳”、“移动”等动作,每个玩家有不同的爱好,所以我们需要让玩家来设置键盘的功能,这样我们需要影射这些通用的游戏行为,于是类InputManager是控件玩家输入行为:

  • 处理所有的键盘和鼠标事件,包括相关的鼠标行为
  • 保存这些事件,这样我们可以当我们需要时精确查询这些事件,而不修改AWT事件分发线程中的游戏状态
  • 检查初始化过的键盘按下事件,然后检查该键值是否已经被其它的键位占用了
  • 影射键盘到游戏的通用行为,比如把空格键影射成为“跳”的行为
  • 可以让用户任何配置键盘的行为

以上功能我们使用GameAction类来封装,其中isPressed()是判断键盘的行为,而getAmount()是判断鼠标移动了多少。最后,这些方法由InputManager来呼叫,比如在这个类的press()和release()方法中呼叫GameAction中的接口方法。

演示代码-GameAction

package com.az.arklis.engine;

/**
	功能:该类是用户初始行为的抽象(定义),比如跳和移动。该类由InputManager类用来影射
		  键盘和鼠标的行为。
	备注:所谓游戏输入行为包括在游戏循环中的特定点的输入,我们可以设置一个boolean变量用来表示一个
		  键是否按下了。比如设置一个jumpIsPressed布尔变量,把这个变量放到keyPressed()方法中,我们来判断
		  当按下space键之后,我们检查jumpIsPressed是否为true值,如果是true值,那么让玩家执行跳的动作。
		  除了游戏中跳之外,玩家还可以设置初始的动作键,比如移动,我们可以设置光标键来表示,以及A键
		  和D键也表示左右移动。假设我们希望玩家自己定义游戏中的行为键,那么,在程序中我们必须实现这些
		  游戏行为的影射功能。我们实现InputManager类来抽象这些行为。总之,我们希望该类InputManager可以
		  完成以下功能:
		  1、处理所有键和鼠标事件,包括鼠标的相对移动
		  2、保存所有上述行为的事件队列,而不是修改AWT事件分发线程的状态
		  3、检查键的初始按下行为,以及检查这些键是否被其它对象占用
		  4、影射所有的游戏行为,比如影射space键为游戏中的跳的动作
		  5、实现可以让玩家自己修改游戏键
	      而GameAction类是用来专门影射游戏中的行为的,也就是抽象游戏行为的设置功能。比如,抽象玩家
		的初始行为(跳或者移动)。该类被InputManager类使用来影射键盘和鼠标的行为。
	*/
public class GameAction{
	//普通行为--针对isPressed()方法返回的true值来表示,即表示一个键已经被占用。
	public static final int NORMAL = 0;
	/*
		初始化按键行为,isPressed()方法返回true值的情况是:只有该键第一次被被按下之后,并且不是该键
		在被释放之后再按下的状态。
		*/
	public static final int DETECT_INITIAL_PRESS_ONLY = 1;
	
	private static final int STATE_RELEASED = 0;//标识是否被释放
	private static final int STATE_PRESSED = 1; //标识是否处理按下的状态
	private static final int STATE_WAITING_FOR_RELEASE = 2; //标识是否等待释放的状态
	
	private String name;//保存游戏行为的名称
	private int behavior; //表示游戏的行为
	private int amount; //计数器
	private int state; //当前状态标识
	
	/**
		在构造方法中初始化成员变量--游戏行为名称,以及普通状态。
		*/
	public GameAction(String name){
		this(name,NORMAL);
	}
	
	public int getBehavior(){
		return behavior;
	}
	
	/**
		该构造方法指定了游戏的行为
		*/
	public GameAction(String name, int behavior){
		this.name = name;
		this.behavior = behavior;
		reset();//回到释放状态,然后计数器清零
	}
	
	public String getName(){
		return name;
	}
	
	public void setName(String name){
		this.name = name;
	}
	
	public void reset(){
		state = STATE_RELEASED;
		amount = 0;
	}
	
	/**
		功能:开关该GameAction行为--等同于press之后release行为
		*/
	public synchronized void tap(){
		press();
		release();
	}
	
	/**
		功能:标识键盘被点击事件
		*/
	public synchronized void press(){
		press(1);
	}
	
	/**
		功能:表示该键被指定点击的次数,鼠标移动到指定位置
		*/
	public synchronized void press(int amount){
		if(state != STATE_WAITING_FOR_RELEASE){
			this.amount += amount;
			state = STATE_PRESSED;
		}
	}
	
	public synchronized void release(){
		state = STATE_RELEASED;
	}
	
	public synchronized boolean isPressed(){
		return (getAmount() != 0);
	}
	
	public synchronized int getAmount(){
		int returnValue = amount;
		if(returnValue != 0){
			if(state == STATE_RELEASED){
				amount = 0;
			}else if(behavior == DETECT_INITIAL_PRESS_ONLY){
				state = STATE_WAITING_FOR_RELEASE;
				amount = 0;
			}
		}
		return returnValue;
	}
}

最后,我们创建一个InputManager类用来管理所有输入,并发等待不见光标和相关的鼠标行和等。另外该类有影射键盘和鼠标事件到GameAction类中,当我们按下一个键盘时,该类的代码检查GameAction是否有键盘被影射了,如果有那么呼叫GameAction类的中press()方法。

那么在这个类中怎样影射?我们使用一个GameAction数组来解决,每个下标对应一个虚拟键代码,最大虚拟键的只能小于或者等于600数值,也就是说GameAction数组的长度是600.

使用输入管理器

下面,我们创建一个hero可以左右移动,以及跳跃的行为;另外我们可以该应用程序添加暂停功能,不过这不是一个真正的游戏。其中,在人物跳跃时需要表现重力—人物会回到地面的感觉。这里需要说明的是,地球引力(gravity)是9.8米/秒,但是游戏中,这个不重要,我们使用像素来表示,所以定义引用为0.002像素/秒,这样做防止人物跳出屏幕的顶端。

演示代码-Player

package com.az.arklis.games;
import static java.lang.System.*;
import com.az.arklis.engine.*;

/**
	功能:书写一个玩家类,用来实现跳和产生引力的行为
	*/

public class Player extends Sprite{
	public static final int STATE_NORMAL = 0;
	public static final int STATE_JUMPING = 1;
	//public static final float SPEED = .3F; //根据图像大小来调整速率
	public static final float SPEED = 0.6F;
	//public static final float GRAVITY = .002F; //加速度调整为0
	public static final float GRAVITY = .002F;
	
	private int floorY;
	private int state;
	
	public Player(Animation animation){
		super(animation);
		state = STATE_NORMAL;
	}
	
	public int getState(){
		return state;
	}
	
	public void setState(int state){
		this.state = state;
	}
	
	/**
		设置floor的位置,不管玩家是否开始跳,或者已经着陆
		*/
	public void setFloorY(int floorY){
		this.floorY = floorY;
		setY(floorY);
	}
	
	/**
		让玩家产生的跳的动作
		*/
	public void jump(){
		setVelocityY(-1);
		state = STATE_JUMPING;
	}
	
	/**
		更新玩家的位置和动作,也可以设置玩家的状态为NORMAL状态,如果玩家已经着陆了
		*/
	public void update(long elapsedTime){
		//设置水平位置的速率(下降效果)
		if(getState() == STATE_JUMPING){
			setVelocityY(getVelocityY() + GRAVITY * elapsedTime);
		}
		//移动玩家
		super.update(elapsedTime);
		//检查玩家是否着陆
		if(getState() == STATE_JUMPING && getY() >= floorY){
			setVelocityY(0);
			setY(floorY);
			setState(STATE_NORMAL);
		}
	}
}

Player类是基于状态的类(state-based),它有两种状态:NORMAL和JUMPING状态。因为Player类保存了它们状态,所以它可以检查玩家是否普通状态,还是跳跃状态,以及玩家是否下落的状态。程序中必须告知”floor”在哪里。现在我们已经有了实现简单游戏的条件(使用InputManager类),在类InputManagerTest类中演示怎样移到玩家,然后让玩家跳跃。为实现以上功能,在代码中需要创建几个GameAction来实现该功能,每个GameAction至少影射了一个键盘或者鼠标事件,最后允许我们暂停游戏。

运行效果

Java游戏编程不完全详解-3_游戏开发_04

设计直观的用户界面

在看以上示例之后,我们来完成游戏非常的东西:用户界面。用户界面不仅是按键和鼠标的移动;它还包括了打开菜单,屏幕配置、进入游戏和屏幕按钮。那么设计出直观的、实用的和吸引用户的界面是一个游戏中非常重要的环节。因为,如果没有好用的界面会让游戏失去可欣赏性。

那么用户界面设计原则如下:

  • 保证界面简单和整洁。不是所有选项都一次呈现出来,相反,应该把最常用的、最有用的选项放在主屏幕中,以方便玩家使用。
  • 确保每个选项按钮都可以非常容易使用,如果需要点击很多次才能找到确定的功能,那么会让玩家很不开心。
  • 使用提示贴士(tool tips)。一般提示贴士是鼠标经过特定对象时弹出来的形式,这样它们可以告诉玩家哪些按钮在屏幕中做功能,以及它们的当前的状态。提示贴士可以非常快捷的回答“What’s this?”的问题,因为Swing中有一个内置tooltip功能,所以非常容易实现该功能。
  • 每个游戏行为都响应玩家一个信息,比如使用声音或者等待光标来表示等。
  • 测试你的用户界面。因为有些按钮对于我们来说可能是最明显的,但是对于别人可能就不是了。所以,我们需要测试有多少人习惯我们这样的按钮设计方式。当然,不可能让所有人都满意,但是需要大多数人习惯都可行。记住,在实现生活中,当玩家使用我们的游戏时,我们不会告诉他/她下一步应该做什么!
  • 当游戏界面测试之后,调查这些玩家,他们认为这些按钮哪些最容易使用和最有用。比如哪些图标最容易让他们辨认等。但是,我们只是听,而不用考虑代码实现上的难度。
  • 重新修改游戏界面,如果不能运行,那么需要花几天时间来编码,以及创建新的图标,以做出完美的界面。
  • 使用Swing组件来开始用户界面:Swing是一个非常大的话题,简而言之,Swing是一组类,它们被用来创建用户界面元素,比如窗体、按钮、弹出菜单、下拉列表文本输入框、选项按钮和标签等。比如,前面我们使用JFrame类来实现全屏幕显示功能。实际上,我们使用JFrame对象来操作Windw和Frame而已。

一些Swing组件使用自己的组件来呈现画面,所以,我们可以在自己的呈现循环中使用Swing组件。这是一个令人振奋的消息,因为我们可以把所有的Swing功能都整合到全屏幕游戏中去。这样,我们可以不必重新造轮子来创建用户界面框架了!同时,Swing可以根据自定义样式来客制化UI界面,所以,我们可以使用Swing实现个性化的界面。Swing有大量的API可以使用,而这里讲解的是游戏,不是Swing组件,所以不会讨论Swing的组件功能。在游戏中使用Swing组件的技巧如下:

绘制所有的Swing组件时,只需要在动画循环过程中,呼叫主面板的paintComponents()方法即可:

//绘制我们的图形
draw(g);
//绘制Swing组件
JFrame frame = screen.getFullScreenWindow();
frame.getLayeredPane().paintComponents(g);

有可能出现的问题是:内容面板实际上绘制它的背景,所以它会隐藏面板下面的所有内容;如果希望自已Swing组件以独立的形式呈现,那么需要把内容面板设置为透明:

If(contentPane instanceof JComponent){
      ((JComponent)contentPane).setOpaque(false);
}

第二问题是:处理Swing怎样呈现自己的组件问题,因为普通的Swing应用,我们不必呼叫paintComponents()方法—Swing会在AWT事件分发线程中自动呈现所有的组件。而现在我们必须手动关闭这个功能,从而达到控制组件的呈现时机,比如按一个按钮时,我们让该按钮呈现被按下的样子,该组件需要呼叫repaint()方法来重绘组件的样子。显然,我们不需要AWT分发线程中出现组件呈现,因为这与我们的自定义的画面呈现会发生冲突,会产生抖动或者其它的冲突现象。

在我们的代码中,需要忽略潜在的重绘请求,如果一个按钮被按下之后的话,然后需要在动画循环的draw方法中出现。为解决这个问题,需要我们捕获重绘请求,然后忽略它。根据此思路,因为所有的重绘请求都发送到RepaintManager类,所以我们通过该类来管理重绘请求,然后把这些请求分发到实际需要处理重绘的组件上去。于是,我们需要简单使用NullRepaintManger来重写RepaintManger类即可实现。

演示代码-NullRepaintManer

package com.az.arklis.engine;
import static java.lang.System.*;
import javax.swing.RepaintManager;
import javax.swing.JComponent;

/**
	功能:书写一个管理Swing中组件重绘请求的工具类
	备注:
		  截获组件的重绘请求,然后根据实际动画需求分发到相应的组件中去
	*/

public class NullRepaintManager extends RepaintManager{
	/**
		安装非重绘管理器
		*/
	public static void install(){
		RepaintManager repaintManager = new NullRepaintManager();
		//不准使用双缓存显示策略
		repaintManager.setDoubleBufferingEnabled(false);
		//修改当前的重绘管理器为repaintManager对象
		RepaintManager.setCurrentManager(repaintManager);
	}
	
	/**
		功能:指定某组件无效--需要子类重写来确定
		*/
	public void addInvalidComponent(JComponent component){
		//do nothing
	}
	
	/**
		功能:指定某组件无效区--需要子类来重写确定
		*/
	public void addDirtyRegion(JComponent component, int x, int y, int w, int h){
		//do nothing
	}
	
	/**
		功能:指定某组件无效区标识--需要子类来重写确定
		*/
	public void markCompletelyDirty(JComponent component){
		//do nothing
	}
	
	/**
		功能:指定某组件绘制区--需要子类来重写确定
		*/
	public void paintDirtyRegions(){
		//do nothing
	}
}

该类继承RepaintManager类,然后重写关键方法—什么都不做,于是重绘事件不会被发送到AWT分发线程中去,所以我们不会看抖动的组件画面。

API代码-RepaintManager

/*
 * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package javax.swing;


import java.awt.*;
import java.awt.event.*;
import java.awt.image.VolatileImage;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.applet.*;

import jdk.internal.access.JavaSecurityAccess;
import jdk.internal.access.SharedSecrets;
import sun.awt.AWTAccessor;
import sun.awt.AppContext;
import sun.awt.DisplayChangedListener;
import sun.awt.SunToolkit;
import sun.java2d.SunGraphicsEnvironment;
import sun.security.action.GetPropertyAction;

import com.sun.java.swing.SwingUtilities3;
import java.awt.geom.AffineTransform;
import sun.java2d.SunGraphics2D;
import sun.java2d.pipe.Region;
import sun.swing.SwingAccessor;
import sun.swing.SwingUtilities2;
import sun.swing.SwingUtilities2.RepaintListener;

/**
 * This class manages repaint requests, allowing the number
 * of repaints to be minimized, for example by collapsing multiple
 * requests into a single repaint for members of a component tree.
 * <p>
 * As of 1.6 <code>RepaintManager</code> handles repaint requests
 * for Swing's top level components (<code>JApplet</code>,
 * <code>JWindow</code>, <code>JFrame</code> and <code>JDialog</code>).
 * Any calls to <code>repaint</code> on one of these will call into the
 * appropriate <code>addDirtyRegion</code> method.
 *
 * @author Arnaud Weber
 * @since 1.2
 */
public class RepaintManager
{
    /**
     * Whether or not the RepaintManager should handle paint requests
     * for top levels.
     */
    static final boolean HANDLE_TOP_LEVEL_PAINT;

    private static final short BUFFER_STRATEGY_NOT_SPECIFIED = 0;
    private static final short BUFFER_STRATEGY_SPECIFIED_ON = 1;
    private static final short BUFFER_STRATEGY_SPECIFIED_OFF = 2;

    private static final short BUFFER_STRATEGY_TYPE;

    /**
     * Maps from GraphicsConfiguration to VolatileImage.
     */
    private Map<GraphicsConfiguration,VolatileImage> volatileMap = new
                        HashMap<GraphicsConfiguration,VolatileImage>(1);

    //
    // As of 1.6 Swing handles scheduling of paint events from native code.
    // That is, SwingPaintEventDispatcher is invoked on the toolkit thread,
    // which in turn invokes nativeAddDirtyRegion.  Because this is invoked
    // from the native thread we can not invoke any public methods and so
    // we introduce these added maps.  So, any time nativeAddDirtyRegion is
    // invoked the region is added to hwDirtyComponents and a work request
    // is scheduled.  When the work request is processed all entries in
    // this map are pushed to the real map (dirtyComponents) and then
    // painted with the rest of the components.
    //
    private Map<Container,Rectangle> hwDirtyComponents;

    private Map<Component,Rectangle> dirtyComponents;
    private Map<Component,Rectangle> tmpDirtyComponents;
    private java.util.List<Component> invalidComponents;

    // List of Runnables that need to be processed before painting from AWT.
    private java.util.List<Runnable> runnableList;

    boolean   doubleBufferingEnabled = true;

    private Dimension doubleBufferMaxSize;

    private boolean isCustomMaxBufferSizeSet = false;

    // Support for both the standard and volatile offscreen buffers exists to
    // provide backwards compatibility for the [rare] programs which may be
    // calling getOffScreenBuffer() and not expecting to get a VolatileImage.
    // Swing internally is migrating to use *only* the volatile image buffer.

    // Support for standard offscreen buffer
    //
    DoubleBufferInfo standardDoubleBuffer;

    /**
     * Object responsible for hanlding core paint functionality.
     */
    private PaintManager paintManager;

    private static final Object repaintManagerKey = RepaintManager.class;

    // Whether or not a VolatileImage should be used for double-buffered painting
    static boolean volatileImageBufferEnabled = true;
    /**
     * Type of VolatileImage which should be used for double-buffered
     * painting.
     */
    private static final int volatileBufferType;
    /**
     * Value of the system property awt.nativeDoubleBuffering.
     */
    private static boolean nativeDoubleBuffering;

    // The maximum number of times Swing will attempt to use the VolatileImage
    // buffer during a paint operation.
    private static final int VOLATILE_LOOP_MAX = 2;

    /**
     * Number of <code>beginPaint</code> that have been invoked.
     */
    private int paintDepth = 0;

    /**
     * Type of buffer strategy to use.  Will be one of the BUFFER_STRATEGY_
     * constants.
     */
    private short bufferStrategyType;

    //
    // BufferStrategyPaintManager has the unique characteristic that it
    // must deal with the buffer being lost while painting to it.  For
    // example, if we paint a component and show it and the buffer has
    // become lost we must repaint the whole window.  To deal with that
    // the PaintManager calls into repaintRoot, and if we're still in
    // the process of painting the repaintRoot field is set to the JRootPane
    // and after the current JComponent.paintImmediately call finishes
    // paintImmediately will be invoked on the repaintRoot.  In this
    // way we don't try to show garbage to the screen.
    //
    /**
     * True if we're in the process of painting the dirty regions.  This is
     * set to true in <code>paintDirtyRegions</code>.
     */
    private boolean painting;
    /**
     * If the PaintManager calls into repaintRoot during painting this field
     * will be set to the root.
     */
    private JComponent repaintRoot;

    /**
     * The Thread that has initiated painting.  If null it
     * indicates painting is not currently in progress.
     */
    private Thread paintThread;

    /**
     * Runnable used to process all repaint/revalidate requests.
     */
    private final ProcessingRunnable processingRunnable;

    private static final JavaSecurityAccess javaSecurityAccess =
            SharedSecrets.getJavaSecurityAccess();

    /**
     * Listener installed to detect display changes. When display changes,
     * schedules a callback to notify all RepaintManagers of the display
     * changes.
     */
    private static final DisplayChangedListener displayChangedHandler =
            new DisplayChangedHandler();

    static {
        SwingAccessor.setRepaintManagerAccessor(new SwingAccessor.RepaintManagerAccessor() {
            @Override
            public void addRepaintListener(RepaintManager rm, RepaintListener l) {
                rm.addRepaintListener(l);
            }
            @Override
            public void removeRepaintListener(RepaintManager rm, RepaintListener l) {
                rm.removeRepaintListener(l);
            }
        });

        volatileImageBufferEnabled = "true".equals(AccessController.
                doPrivileged(new GetPropertyAction(
                "swing.volatileImageBufferEnabled", "true")));
        boolean headless = GraphicsEnvironment.isHeadless();
        if (volatileImageBufferEnabled && headless) {
            volatileImageBufferEnabled = false;
        }
        nativeDoubleBuffering = "true".equals(AccessController.doPrivileged(
                    new GetPropertyAction("awt.nativeDoubleBuffering")));
        String bs = AccessController.doPrivileged(
                          new GetPropertyAction("swing.bufferPerWindow"));
        if (headless) {
            BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF;
        }
        else if (bs == null) {
            BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_NOT_SPECIFIED;
        }
        else if ("true".equals(bs)) {
            BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_ON;
        }
        else {
            BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF;
        }
        HANDLE_TOP_LEVEL_PAINT = "true".equals(AccessController.doPrivileged(
               new GetPropertyAction("swing.handleTopLevelPaint", "true")));
        GraphicsEnvironment ge = GraphicsEnvironment.
                getLocalGraphicsEnvironment();
        if (ge instanceof SunGraphicsEnvironment) {
            ((SunGraphicsEnvironment) ge).addDisplayChangedListener(
                    displayChangedHandler);
        }
        Toolkit tk = Toolkit.getDefaultToolkit();
        if ((tk instanceof SunToolkit)
                && ((SunToolkit) tk).isSwingBackbufferTranslucencySupported()) {
            volatileBufferType = Transparency.TRANSLUCENT;
        } else {
            volatileBufferType = Transparency.OPAQUE;
        }
    }

   // 代码太多,我们这里就省去一些,因为掘金在浏览器编辑中说超过了字数,所以我们就省去了很多...
   
   

    /**
     * If possible this will show a previously rendered portion of
     * a Component.  If successful, this will return true, otherwise false.
     * <p>
     * WARNING: This method is invoked from the native toolkit thread, be
     * very careful as to what methods this invokes!
     */
    boolean show(Container c, int x, int y, int w, int h) {
        return getPaintManager().show(c, x, y, w, h);
    }

    /**
     * Invoked when the doubleBuffered or useTrueDoubleBuffering
     * properties of a JRootPane change.  This may come in on any thread.
     */
    void doubleBufferingChanged(JRootPane rootPane) {
        getPaintManager().doubleBufferingChanged(rootPane);
    }

    /**
     * Sets the <code>PaintManager</code> that is used to handle all
     * double buffered painting.
     *
     * @param paintManager The PaintManager to use.  Passing in null indicates
     *        the fallback PaintManager should be used.
     */
    void setPaintManager(PaintManager paintManager) {
        if (paintManager == null) {
            paintManager = new PaintManager();
        }
        PaintManager oldPaintManager;
        synchronized(this) {
            oldPaintManager = this.paintManager;
            this.paintManager = paintManager;
            paintManager.repaintManager = this;
        }
        if (oldPaintManager != null) {
            oldPaintManager.dispose();
        }
    }

    private synchronized PaintManager getPaintManager() {
        if (paintManager == null) {
            PaintManager paintManager = null;
            if (doubleBufferingEnabled && !nativeDoubleBuffering) {
                switch (bufferStrategyType) {
                case BUFFER_STRATEGY_NOT_SPECIFIED:
                    Toolkit tk = Toolkit.getDefaultToolkit();
                    if (tk instanceof SunToolkit) {
                        SunToolkit stk = (SunToolkit) tk;
                        if (stk.useBufferPerWindow()) {
                            paintManager = new BufferStrategyPaintManager();
                        }
                    }
                    break;
                case BUFFER_STRATEGY_SPECIFIED_ON:
                    paintManager = new BufferStrategyPaintManager();
                    break;
                default:
                    break;
                }
            }
            // null case handled in setPaintManager
            setPaintManager(paintManager);
        }
        return paintManager;
    }

    private void scheduleProcessingRunnable(AppContext context) {
        if (processingRunnable.markPending()) {
            Toolkit tk = Toolkit.getDefaultToolkit();
            if (tk instanceof SunToolkit) {
                SunToolkit.getSystemEventQueueImplPP(context).
                  postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(),
                                                processingRunnable));
            } else {
                Toolkit.getDefaultToolkit().getSystemEventQueue().
                      postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(),
                                                    processingRunnable));
            }
        }
    }


    /**
     * PaintManager is used to handle all double buffered painting for
     * Swing.  Subclasses should call back into the JComponent method
     * <code>paintToOffscreen</code> to handle the actual painting.
     */
    static class PaintManager {
        /**
         * RepaintManager the PaintManager has been installed on.
         */
        protected RepaintManager repaintManager;
        boolean isRepaintingRoot;

        /**
         * Paints a region of a component
         *
         * @param paintingComponent Component to paint
         * @param bufferComponent Component to obtain buffer for
         * @param g Graphics to paint to
         * @param x X-coordinate
         * @param y Y-coordinate
         * @param w Width
         * @param h Height
         * @return true if painting was successful.
         */
        public boolean paint(JComponent paintingComponent,
                             JComponent bufferComponent, Graphics g,
                             int x, int y, int w, int h) {
            // First attempt to use VolatileImage buffer for performance.
            // If this fails (which should rarely occur), fallback to a
            // standard Image buffer.
            boolean paintCompleted = false;
            Image offscreen;
            int sw = w + 1;
            int sh = h + 1;

            if (repaintManager.useVolatileDoubleBuffer() &&
                (offscreen = getValidImage(repaintManager.
                getVolatileOffscreenBuffer(bufferComponent, sw, sh))) != null) {
                VolatileImage vImage = (java.awt.image.VolatileImage)offscreen;
                GraphicsConfiguration gc = bufferComponent.
                                            getGraphicsConfiguration();
                for (int i = 0; !paintCompleted &&
                         i < RepaintManager.VOLATILE_LOOP_MAX; i++) {
                    if (vImage.validate(gc) ==
                                   VolatileImage.IMAGE_INCOMPATIBLE) {
                        repaintManager.resetVolatileDoubleBuffer(gc);
                        offscreen = repaintManager.getVolatileOffscreenBuffer(
                            bufferComponent, sw, sh);
                        vImage = (java.awt.image.VolatileImage)offscreen;
                    }
                    paintDoubleBuffered(paintingComponent, vImage, g, x, y,
                                        w, h);
                    paintCompleted = !vImage.contentsLost();
                }
            }
            // VolatileImage painting loop failed, fallback to regular
            // offscreen buffer
            if (!paintCompleted && (offscreen = getValidImage(
                      repaintManager.getOffscreenBuffer(
                      bufferComponent, w, h))) != null) {
                paintDoubleBuffered(paintingComponent, offscreen, g, x, y, w,
                                    h);
                paintCompleted = true;
            }
            return paintCompleted;
        }

        /**
         * Does a copy area on the specified region.
         */
        public void copyArea(JComponent c, Graphics g, int x, int y, int w,
                             int h, int deltaX, int deltaY, boolean clip) {
            g.copyArea(x, y, w, h, deltaX, deltaY);
        }

        /**
         * Invoked prior to any calls to paint or copyArea.
         */
        public void beginPaint() {
        }

        /**
         * Invoked to indicate painting has been completed.
         */
        public void endPaint() {
        }

        /**
         * Shows a region of a previously rendered component.  This
         * will return true if successful, false otherwise.  The default
         * implementation returns false.
         */
        public boolean show(Container c, int x, int y, int w, int h) {
            return false;
        }

        /**
         * Invoked when the doubleBuffered or useTrueDoubleBuffering
         * properties of a JRootPane change.  This may come in on any thread.
         */
        public void doubleBufferingChanged(JRootPane rootPane) {
        }

        /**
         * Paints a portion of a component to an offscreen buffer.
         */
        protected void paintDoubleBuffered(JComponent c, Image image,
                Graphics g, int clipX, int clipY,
                int clipW, int clipH) {
            if (image instanceof VolatileImage && isPixelsCopying(c, g)) {
                paintDoubleBufferedFPScales(c, image, g, clipX, clipY, clipW, clipH);
            } else {
                paintDoubleBufferedImpl(c, image, g, clipX, clipY, clipW, clipH);
            }
        }

        private void paintDoubleBufferedImpl(JComponent c, Image image,
                                             Graphics g, int clipX, int clipY,
                                             int clipW, int clipH) {
            Graphics osg = image.getGraphics();
            int bw = Math.min(clipW, image.getWidth(null));
            int bh = Math.min(clipH, image.getHeight(null));
            int x,y,maxx,maxy;

            try {
                for(x = clipX, maxx = clipX+clipW; x < maxx ;  x += bw ) {
                    for(y=clipY, maxy = clipY + clipH; y < maxy ; y += bh) {
                        osg.translate(-x, -y);
                        osg.setClip(x,y,bw,bh);
                        if (volatileBufferType != Transparency.OPAQUE
                                && osg instanceof Graphics2D) {
                            final Graphics2D g2d = (Graphics2D) osg;
                            final Color oldBg = g2d.getBackground();
                            g2d.setBackground(c.getBackground());
                            g2d.clearRect(x, y, bw, bh);
                            g2d.setBackground(oldBg);
                        }
                        c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);
                        g.setClip(x, y, bw, bh);
                        if (volatileBufferType != Transparency.OPAQUE
                                && g instanceof Graphics2D) {
                            final Graphics2D g2d = (Graphics2D) g;
                            final Composite oldComposite = g2d.getComposite();
                            g2d.setComposite(AlphaComposite.Src);
                            g2d.drawImage(image, x, y, c);
                            g2d.setComposite(oldComposite);
                        } else {
                            g.drawImage(image, x, y, c);
                        }
                        osg.translate(x, y);
                    }
                }
            } finally {
                osg.dispose();
            }
        }

        private void paintDoubleBufferedFPScales(JComponent c, Image image,
                                                 Graphics g, int clipX, int clipY,
                                                 int clipW, int clipH) {
            Graphics osg = image.getGraphics();
            Graphics2D g2d = (Graphics2D) g;
            Graphics2D osg2d = (Graphics2D) osg;

            AffineTransform identity = new AffineTransform();
            int bw = Math.min(clipW, image.getWidth(null));
            int bh = Math.min(clipH, image.getHeight(null));
            int x, y, maxx, maxy;

            AffineTransform tx = g2d.getTransform();
            double scaleX = tx.getScaleX();
            double scaleY = tx.getScaleY();
            double trX = tx.getTranslateX();
            double trY = tx.getTranslateY();

            boolean translucent = volatileBufferType != Transparency.OPAQUE;
            Composite oldComposite = g2d.getComposite();

            try {
                for (x = clipX, maxx = clipX + clipW; x < maxx; x += bw) {
                    for (y = clipY, maxy = clipY + clipH; y < maxy; y += bh) {

                        // draw x, y, bw, bh
                        int pixelx1 = Region.clipRound(x * scaleX + trX);
                        int pixely1 = Region.clipRound(y * scaleY + trY);
                        int pixelx2 = Region.clipRound((x + bw) * scaleX + trX);
                        int pixely2 = Region.clipRound((y + bh) * scaleY + trY);
                        int pixelw = pixelx2 - pixelx1;
                        int pixelh = pixely2 - pixely1;

                        osg2d.setTransform(identity);
                        if (translucent) {
                            final Color oldBg = g2d.getBackground();
                            g2d.setBackground(c.getBackground());
                            g2d.clearRect(pixelx1, pixely1, pixelw, pixelh);
                            g2d.setBackground(oldBg);
                        }

                        osg2d.setClip(0, 0, pixelw, pixelh);
                        osg2d.translate(trX - pixelx1, trY - pixely1);
                        osg2d.scale(scaleX, scaleY);
                        c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);

                        g2d.setTransform(identity);
                        g2d.setClip(pixelx1, pixely1, pixelw, pixelh);
                        AffineTransform stx = new AffineTransform();
                        stx.translate(pixelx1, pixely1);
                        stx.scale(scaleX, scaleY);
                        g2d.setTransform(stx);

                        if (translucent) {
                            g2d.setComposite(AlphaComposite.Src);
                        }

                        g2d.drawImage(image, 0, 0, c);

                        if (translucent) {
                            g2d.setComposite(oldComposite);
                        }
                        g2d.setTransform(tx);
                    }
                }
            } finally {
                osg.dispose();
            }
        }

        /**
         * If <code>image</code> is non-null with a positive size it
         * is returned, otherwise null is returned.
         */
        private Image getValidImage(Image image) {
            if (image != null && image.getWidth(null) > 0 &&
                                 image.getHeight(null) > 0) {
                return image;
            }
            return null;
        }

        /**
         * Schedules a repaint for the specified component.  This differs
         * from <code>root.repaint</code> in that if the RepaintManager is
         * currently processing paint requests it'll process this request
         * with the current set of requests.
         */
        protected void repaintRoot(JComponent root) {
            assert (repaintManager.repaintRoot == null);
            if (repaintManager.painting) {
                repaintManager.repaintRoot = root;
            }
            else {
                root.repaint();
            }
        }

        /**
         * Returns true if the component being painted is the root component
         * that was previously passed to <code>repaintRoot</code>.
         */
        protected boolean isRepaintingRoot() {
            return isRepaintingRoot;
        }

        /**
         * Cleans up any state.  After invoked the PaintManager will no
         * longer be used anymore.
         */
        protected void dispose() {
        }

        private boolean isPixelsCopying(JComponent c, Graphics g) {

            AffineTransform tx = getTransform(g);
            GraphicsConfiguration gc = c.getGraphicsConfiguration();

            if (tx == null || gc == null
                    || !SwingUtilities2.isFloatingPointScale(tx)) {
                return false;
            }

            AffineTransform gcTx = gc.getDefaultTransform();

            return gcTx.getScaleX() == tx.getScaleX()
                    && gcTx.getScaleY() == tx.getScaleY();
        }

        private static AffineTransform getTransform(Graphics g) {
            if (g instanceof SunGraphics2D) {
                return ((SunGraphics2D) g).transform;
            } else if (g instanceof Graphics2D) {
                return ((Graphics2D) g).getTransform();
            }
            return null;
        }
    }

    private class DoubleBufferInfo {
        public Image image;
        public Dimension size;
        public boolean needsReset = false;
    }


    /**
     * Listener installed to detect display changes. When display changes,
     * schedules a callback to notify all RepaintManagers of the display
     * changes. Only one DisplayChangedHandler is ever installed. The
     * singleton instance will schedule notification for all AppContexts.
     */
    private static final class DisplayChangedHandler implements
                                             DisplayChangedListener {
        public void displayChanged() {
            scheduleDisplayChanges();
        }

        public void paletteChanged() {
        }

        private static void scheduleDisplayChanges() {
            // To avoid threading problems, we notify each RepaintManager
            // on the thread it was created on.
            for (AppContext context : AppContext.getAppContexts()) {
                synchronized(context) {
                    if (!context.isDisposed()) {
                        EventQueue eventQueue = (EventQueue)context.get(
                            AppContext.EVENT_QUEUE_KEY);
                        if (eventQueue != null) {
                            eventQueue.postEvent(new InvocationEvent(
                                Toolkit.getDefaultToolkit(),
                                new DisplayChangedRunnable()));
                        }
                    }
                }
            }
        }
    }


    private static final class DisplayChangedRunnable implements Runnable {
        public void run() {
            RepaintManager.currentManager((JComponent)null).displayChanged();
        }
    }


    /**
     * Runnable used to process all repaint/revalidate requests.
     */
    private final class ProcessingRunnable implements Runnable {
        // If true, we're wainting on the EventQueue.
        private boolean pending;

        /**
         * Marks this processing runnable as pending. If this was not
         * already marked as pending, true is returned.
         */
        public synchronized boolean markPending() {
            if (!pending) {
                pending = true;
                return true;
            }
            return false;
        }

        public void run() {
            synchronized (this) {
                pending = false;
            }
            // First pass, flush any heavy paint events into real paint
            // events.  If there are pending heavy weight requests this will
            // result in q'ing this request up one more time.  As
            // long as no other requests come in between now and the time
            // the second one is processed nothing will happen.  This is not
            // ideal, but the logic needed to suppress the second request is
            // more headache than it's worth.
            scheduleHeavyWeightPaints();
            // Do the actual validation and painting.
            validateInvalidComponents();
            prePaintDirtyRegions();
        }
    }
    private RepaintManager getDelegate(Component c) {
        RepaintManager delegate = SwingUtilities3.getDelegateRepaintManager(c);
        if (this == delegate) {
            delegate = null;
        }
        return delegate;
    }
}

我们从1895行的API代码全部展示出来,目的是想让大家阅读一下OpenJDK 15版本的深度封装的代码,从而让自己有一个很好的定位,以及对设计模式和底层原理有所了解。

**注意:**因为Swing组件是非线程安全的,所以当一个Swing组件可见时,我们不可能在AWT事件分发线程中修改它的状态。如果我们需要在Swing组件显示之后修改它,那么需要在事件分发线程这样做:

EventQueue.invokeAndWait(new Runnable(){
	public void run(){
	       doSomething();
    }
});

以上代码是在AWT事件分发器线程器呼叫代码,然后等待这些代码执行完成,另外,如果我们不想等这些代码执行完成,那么呼叫invokeLater()方法来实现,创建一个简单的菜单(Creating a Simple Menu)

现在我们改进一下InputManagerTest类,可以添加一些简单的用户界面:暂停、配置和退出按钮。首先,当我们点击一个按钮时会发生什么? Swing会判断该点击,然后检查该按钮是否有监听器。如果有,那么监听器会通知AWT事件分发器线程,说明该按钮被按下了。在代码中,我们通过ActionEvent的getSource()方法知道哪些组件产生了事件行为。

public void actionPerforme(ActionEvent e){
	Object src = e.getSource();
	if(src == okButton){
		//do something …
	}
}

最后,在用户界面中,我们可以该按钮做以下事件:

  • 添加提示贴士—只需要呼叫setToolTip(“Hello World”)方法 ,然后剩下由Swing来实现
  • 使用图标,而不是在按钮中使用文本。必须两个不同的图标,一个表示初始状态,一个表示按下状态
  • 隐藏默认的样式。如果需要让图标原样出现,那么需要隐藏按钮的边框,隐藏时呼叫setContentAreaFiled(false)方法,以保证挥刀的背景不会被绘制
  • 修改光标。让光标在滑过按钮时变成手样—呼叫setCursor()方法即可
  • 关闭键盘焦点—呼叫setFocusable(false)

演示代码-MenuTest

package com.funfree.arklis.input;
import java.awt.event.*;
import static java.lang.System.*;
import java.awt.*;
import java.util.LinkedList;
import com.funfree.arklis.util.*;
import com.funfree.arklis.engine.*;
import javax.swing.*;

/**
	功能:书写一个类用来测试游戏菜单的使用
	*/
public class MenuTest extends InputManagerTest implements ActionListener{
	protected GameAction configAction;
	private JButton playButton;
	private JButton configButton;
	private JButton quitButton;
	private JButton pauseButton;
	private JPanel playButtonSpace;
	
	public void init(){
		super.init();
		//不让Swing组件重绘
		NullRepaintManager.install();
		//创建一个“配置”对象
		configAction = new GameAction("config");
		//创建菜单按钮
		quitButton = createButton("quit","退出");
		playButton = createButton("play","继续");
		pauseButton = createButton("pause","暂停");
		configButton = createButton("config","修改设置");
		//创建play/pause按钮的空间
		playButtonSpace = new JPanel();
		playButtonSpace.setOpaque(false);
		playButtonSpace.add(pauseButton);
		//取得全屏幕对象
		JFrame frame = (JFrame)super.screen.getFullScreenWindow();
		Container contentPane = frame.getContentPane();
		
		//让面板透明
		if(contentPane instanceof JComponent){
			((JComponent)contentPane).setOpaque(false);
		}
		
		//添加组件到屏幕屏幕面板中去
		contentPane.setLayout(new FlowLayout(FlowLayout.LEFT));
		contentPane.add(playButtonSpace);
		contentPane.add(configButton);
		contentPane.add(quitButton);
		//显示要求系统列出组件
		frame.validate();
	}
	
	/**
		功能:重写InputManagerTest的draw方法,以便绘制所有的Swing组件
		*/
	public void draw(Graphics2D g){
		super.draw(g);
		JFrame frame = (JFrame)super.screen.getFullScreenWindow();
		//列出面板中的弹出信息(tooltips, pop menus)等
		frame.getLayeredPane().paintComponents(g);
	}
	
	/**
		功能:修改pause/play按钮的样子
		*/
	public void setPaused(boolean pause){
		super.setPaused(pause);
		//把面板清空
		playButtonSpace.removeAll();
		//然后根据当前的暂停状态来决定画面的呈现--如果当前是暂停状态
		if(isPaused()){
			//那么显示释放按钮
			playButtonSpace.add(playButton);
		}else{
			//否则显示暂停按钮
			playButtonSpace.add(pauseButton);
		}
	}
	
	public void actionPerformed(ActionEvent event){
		Object source = event.getSource();
		if(source == quitButton){
			super.exit.tap();
		}else if(source == configButton){
			configAction.tap();
		}else if(source == playButton || source == pauseButton){
			super.pause.tap();
		}
	}
	
	/**
		功能:该方法是一个非常重要的辅助方法--用来创建游戏的菜单
		*/
	private JButton createButton(String name, String toolTip){
		//装载图片
		String imagePath = "images/menu/" + name + ".png";
		out.println(imagePath);
		ImageIcon iconRollover = new ImageIcon(imagePath);
		int w = iconRollover.getIconWidth();
		int h = iconRollover.getIconHeight();
		//给当前按钮设置光标的样式
		Cursor cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
		//让默认的图片透明
		Image image = screen.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
		Graphics2D g = (Graphics2D)image.getGraphics();
		Composite alpha = AlphaComposite.getInstance(AlphaComposite.DST_OVER, .5f);
		g.setComposite(alpha);//设置透明度
		g.drawImage(iconRollover.getImage(),0,0,null);
		g.dispose();
		ImageIcon iconDefault = new ImageIcon(image);
		//显示被按下的图片
		image = screen.createCompatibleImage(w,h,Transparency.TRANSLUCENT);
		g = (Graphics2D)image.getGraphics();
		g.drawImage(iconRollover.getImage(),2,2,null);
		g.dispose();
		ImageIcon iconPressed = new ImageIcon(image);
		//创建按钮对象
		JButton button = new JButton();
		button.addActionListener(this);
		button.setIgnoreRepaint(true);
		button.setFocusable(false);
		button.setToolTipText(toolTip);
		button.setBorder(null);
		button.setContentAreaFilled(false);
		button.setCursor(cursor);
		button.setIcon(iconDefault);
		button.setRolloverIcon(iconRollover);
		button.setPressedIcon(iconPressed);
		
		return button;
	}
}

在上面示例中,每个按钮有一个PNG图片,其它图片是在程序启动时生成。默认菜单图片有一点faded样式的图片呈现,实现这样种效果是使用AlphaComposite类的0.5透明效果。该类的setPause()方法用来设置暂停按钮放在JPanel中,该面板还有其它的菜单功能按钮,当用户点击暂停和非暂停动作时,该面板会正确显示相应的按钮。

运行效果

Java游戏编程不完全详解-3_游戏开发_05

让玩家设置键盘

如果需要让玩家影射键盘或者鼠标,所有玩家可以游戏行为和按钮,以及鼠标按钮,这些按钮是被用来表示游戏行为的,而键盘配置可以有两部分:

  • 我们需要创建配置对象框
  • 我们需要创建一个特殊的组件,该组件可以让玩家输入键盘或者鼠标的点击。

对话框列出所有可能的游戏行为,以及相应的指令,该对话框本身是一个JPanel类,而该面板中的可以有一系列组件、面板和布局管理器。

创建一个特殊的输入组件还是比较困难的,因为我们需要该组件能显示哪些键是影射到当前游戏行为的,哪些键可以让玩家作为按键或者鼠标键来修改设置。当这一切完成之后,还需要该组件把键盘焦点送回主游戏窗体。

创建该输入组件的思路是使用JTextField类来接收玩家的输入,因为该组件允许输入任何文字,所以我们可让玩家输入键或者鼠标点击;那么我们需要重写JTextField类的输入方法,一般的作法需要给该实现类添加KeyListener和MouseListener到

输入事件中去,但是这时需要我们使用另外的方式来获取键,当然还其它的方式可以获取输入事件。因为每个Swing组件都是Component类的实例,所以Component类有方法processKeyEvent()和processMouseEvent方法。这些方法就像KeyListener和MouseListener方法一样,我们只需要重写这些方法,然后让输入事件呼叫enableEvents()方法。

演示代码-KeyConfigTest

package com.funfree.arklis.input;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import com.funfree.arklis.engine.*;
import static java.lang.System.*;

/**
	功能:书写一个测试类,用来测试InputComponent组件是否可以正常使用
	备注:该类继承MenuTest类,用来演示添加一个对话框,以便让玩家自定义游戏行为
	*/
public class KeyConfigTest extends MenuTest{
	private static final String INSTRUCTIONS = "<html><h4 style='color:#FF1493'>在一个行为输入框中点击一下,以修改它的键位。"	+ 
		"<br/>一个行为最多有三个关联的键。<br/>按Backspace键清除一个添加的行为键。</h4></html>";
	private JPanel dialog;
	private JButton okButton;
	
	//重写父为原init方法,以初始化该对象
	public void init(){
		super.init();
		setList(new ArrayList());//初始化一个集合,用来装载输入行为
		//创建GameAction和相应的影射键
		JPanel configPanel = new JPanel(new GridLayout(5,2,2,2));
		addActionConfig(configPanel,moveLeft);
		addActionConfig(configPanel,moveRight);
		addActionConfig(configPanel,jump);
		addActionConfig(configPanel,pause);
		addActionConfig(configPanel,exit);
		
		//创建一个面板用来装OK按钮
		JPanel bottomPanel = new JPanel(new FlowLayout());
		okButton = new JButton(" 确定 ");
		okButton.setFocusable(false);
		okButton.addActionListener(this);
		bottomPanel.add(okButton);
		
		//创建一个指令面板
		JPanel topPanel = new JPanel(new FlowLayout());
		topPanel.add(new JLabel(INSTRUCTIONS));
		
		//创建一个对话边框
		Border border = BorderFactory.createLineBorder(Color.black);
		//创建一个对话框
		dialog = new JPanel(new BorderLayout());
		dialog.add(topPanel, BorderLayout.NORTH);
		dialog.add(configPanel,BorderLayout.CENTER);
		dialog.add(bottomPanel,BorderLayout.SOUTH);
		dialog.setBorder(border);
		dialog.setVisible(false);
		dialog.setSize(dialog.getPreferredSize());
		
		//把对话框置中
		dialog.setLocation(
			(screen.getWidth() - dialog.getWidth()) / 2,
			(screen.getHeight() - dialog.getHeight()) / 2);
		//把该对话框设置成一个模型对话框
		screen.getFullScreenWindow().getLayeredPane().add(dialog,JLayeredPane.MODAL_LAYER);
		
	}
	
	/**
		添加有游戏行为的名称,以便让InputComponent使用这些文字来修改被影射的键
		*/
	private void addActionConfig(JPanel configPanel, GameAction action){
		JLabel label = new JLabel(action.getName(), JLabel.RIGHT);
		InputComponent input = new InputComponent(action,this);
		configPanel.add(label);
		configPanel.add(input);
		getList().add(input);//放到集合中保存起来
	}
	
	public void actionPerformed(ActionEvent event){
		super.actionPerformed(event);
		if(event.getSource() == okButton){
			configAction.tap();//隐藏配置对话框
		}
	}
	
	/*
		重写InputManagerTest类中的checkSystemInput方法,以判断是否隐藏和显示对话框
		*/
	public void checkSystemInput(){
		super.checkSystemInput();
		if(configAction.isPressed()){
			//隐藏或者显示配置对话框
			boolean show = !dialog.isVisible();
			dialog.setVisible(show);
			setPaused(show);
		}
	}
}

运行效果

Java游戏编程不完全详解-3_游戏开发_06

InputComponent类可以得到键盘与鼠标的输入,但是它限制一个游戏行为最多三个关联键—这是人为限制的。该把Backspace键作为特殊键,当在InputCompont组件中输入一个键之后,如果接着按Backspce键,然后点击“确定”按钮,那么会删除玩家设定的功能键。

总结

在端游中,键盘、鼠标的用户自定义是标准的配置功能,所有外调输入管理也是标准配置的功能,因此,如果我们要做端游开发,那么对游戏输入控制和管理是必须的。

从Java的角度来说,因为有API的深度封装,所以我们第三方应用开发人员是非常容易实现计算机外设的输入控制的。

图片来源:http://www.laoshoucun.com/ 页游